﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Xml.Linq;
using System.Text.RegularExpressions;
using System.IO;

using IdeaNMR.Web.Core;
using IdeaNMR.Shared;
using IdeaNMR.Shared.Utils;


namespace IdeaNMR.Web.Admin
{
    public class ImportFromLua
    {
        private LogBook<LogMessage> Log;

        private Dictionary<string, string> VarTransforms;

        public struct LogMessage
        {
            public string Msg { get; set; }

            // Negative number means the number is not important
            public int Number { get; set; }
        }
        public ImportFromLua(string folder, out LogBook<LogMessage> log)
        {
            this.Log = log = new LogBook<LogMessage>();
            LogMessage me = new LogMessage() { Msg = "LogBook created.", Number = -1 };
            log.AddEntry(me, LogBookEntryType.General);

            this.VarTransforms = new Dictionary<string, string>();

            string[] transforms = File.ReadAllLines(folder + "LuaToPythonVarTransformation.txt");

            foreach (string pair in transforms)
            {
                if (string.IsNullOrEmpty(pair)) continue;
                Match pms = Regex.Match(pair, @"(?://[.\s]*)|(.+)=(.+)");
                if (!pms.Success)
                {
                    me.Msg = string.Format("Reading Transformation data failed: {0}.", pair);
                    me.Number = -1;
                    log.AddEntry(me, LogBookEntryType.Error);
                    continue;
                }
                string left = pms.Groups[1].Value.Trim(), right = pms.Groups[2].Value.Trim();
                if (!string.IsNullOrEmpty(left) && !string.IsNullOrEmpty(right) && !VarTransforms.ContainsKey(left))
                    this.VarTransforms.Add(left, right);
            }
        }

        /// <summary>
        /// Imports data from NMRGenerator.
        /// </summary>
        /// <param name="masterContent"></param>
        public void ImportFromXML(string masterContent, string username)
        {
            // Initiate a LogBook
            Log.DefaultLevel = 0;

            // Reusable throuout this's function body because struct is value type
            LogMessage me = new LogMessage()
            {
                Msg = "Begining Import From XML.",
                Number = -1
            };
            this.Log.AddEntry(me, LogBookEntryType.Checkpoint);

            #region Parsing XML Fields
            // Read all the objects into a collection AllObjects
            List<XElement> AllObjects = null;
            // Getting AllObjects
            {
                XElement masterElement = XElement.Parse(masterContent);
                if (masterElement.HasElements)
                {
                    AllObjects = masterElement.Elements().ToList<XElement>();
                    me.Msg = string.Format("{0} objects is found in masterContent.", AllObjects.Count);
                    me.Number = -1;
                    Log.AddEntry(me, LogBookEntryType.Status);
                }
                else
                {
                    me.Msg = "masterContent does not have any objects.";
                    me.Number = -1;
                    Log.AddEntry(me, LogBookEntryType.Error);
                }
            }

            // Create a collection of SetupScripts objects with
            // <fld name='Type' type='String'>Macro</fld>
            List<XElement> MacrosObjects = null;

            {
                var macros = from m in AllObjects
                             from x in m.Elements()
                             where x.Attribute("name") != null && x.Attribute("name").Value == "Type" && x.Value == "Macro"
                             from y in m.Elements()
                             where y.Attribute("name") != null && y.Attribute("name").Value == "Macro Name"
                             let mn = y.Value
                             group m by mn into groups
                             select groups.LastOrDefault();

                MacrosObjects = macros.ToList<XElement>();
                me.Msg = string.Format("{0} macro objects identified.", MacrosObjects.Count);
                me.Msg += Environment.NewLine;
                me.Number = -1;
                for (int i = 0; i < MacrosObjects.Count; i++)
                {
                    string n = (from so in MacrosObjects[i].Elements()
                                where so.Attribute("name") != null && so.Attribute("name").Value == "Macro Name"
                                select so.Value).Single();
                    me.Msg += string.Format("   {0}: {1}.", i + 1, n);
                    me.Msg += Environment.NewLine;

                }
                Log.AddEntry(me, LogBookEntryType.Status);
            }
            #endregion

            Dictionary<string, Component> uniqueComponents = new Dictionary<string, Component>();
            using (IdeaNMRdbContainer inmr = new IdeaNMRdbContainer())
            {
                User admin;
                using (UserDataManager udm = UserDataManager.GetForUsername(BuiltinSettings.EngineUsername))
                {
                    admin = udm.GetDetachedUser();
                }
                inmr.Attach(admin);

                me.Number = -1;
                me.Msg = "Traversing through all Macro objects.";
                Log.AddEntry(me, LogBookEntryType.Checkpoint);
                Log.DefaultLevel++;

                // For each Setup:
                for (int objNumber = 0; objNumber < MacrosObjects.Count; objNumber++)
                {
                    XElement setupObject = MacrosObjects[objNumber];

                    // Validate that all essential fields are defined
                    // <fld name='Body' type='Memo'> - must have content
                    string luaSetupContent = null; // Get content here
                    luaSetupContent = (from x in setupObject.Elements()
                                       where x.Attribute("name") != null && x.Attribute("name").Value == "Body"
                                       select x.Value).Single();
                    // Convert content to Python Language
                    // Get the collection of aux components
                    // pythonSetupContent = contain script content converted to Python
                    // relatedCIs is a collection of aux components
                    List<ComponentItem> relatedCIs;
                    //               string pythonSetupContent = ConvertFromLuaToPython(luaSetupContent, relatedCIs);
                    // Here there must be determined name, identifier and type fields for each component

                    #region  HERE CREATE NEW ENTITY FOR SETUP
                    Setup currentSetup = new Setup();
                    currentSetup.Id = Guid.NewGuid();
                    currentSetup.IsPrivate = false;
                    currentSetup.IsDraft = false;
                    currentSetup.Datetime = DateTime.Now;
                    currentSetup.FirstVersion = currentSetup;
                    currentSetup.Template = currentSetup;
                    currentSetup.VersionInfo = "Imported from CARA database";

                    currentSetup.Name = (from so in setupObject.Elements()
                                         where so.Attribute("name") != null && so.Attribute("name").Value == "Macro Name"
                                         select so.Value).Single();
                    if (currentSetup.Name.Length > 50) currentSetup.Name = currentSetup.Name.Substring(0, 49);
                    currentSetup.Info = (from so in setupObject.Elements()
                                         where so.Attribute("name") != null && so.Attribute("name").Value == "Memo"
                                         select so.Value).Single();

                    // Get type from <fld name='Type (CARA)' type='String'>2D TOCSY</fld>
                    currentSetup.Type = (from so in setupObject.Elements()
                                         where so.Attribute("name") != null && so.Attribute("name").Value == "Type (CARA)"
                                         select so.Value).Single();
                    currentSetup.Dimentionality = DimentionalityExtensions.Parse((from so in setupObject.Elements()
                                                                                  where so.Attribute("name") != null
                                                                                  && so.Attribute("name").Value == "Dimension"
                                                                                  select so.Value).Single());

                    currentSetup.Script = ConvertFromLuaToPython(luaSetupContent, out relatedCIs, objNumber);

                    inmr.AddToBaseObjects(currentSetup);
                    me.Msg = string.Format("({0}/{1}) {2} added to ObjectContent. {3} components identified.", objNumber + 1, MacrosObjects.Count, currentSetup.Name, relatedCIs.Count);
                    me.Number = objNumber + 1;
                    Log.AddEntry(me, LogBookEntryType.Status);

                    #endregion

                    // For each component
                    Log.DefaultLevel++;
                    int componentsCounter = 0;
                    foreach (ComponentItem rCI in relatedCIs)
                    {
                        Component rCEntity = null;
                        if (uniqueComponents.ContainsKey(rCI.Name))
                        {
                            rCEntity = uniqueComponents[rCI.Name];
                            me.Msg = string.Format("{0} component is already added to the objectcontent.", rCI.Name);
                            me.Number = objNumber + 1;
                            Log.AddEntry(me, LogBookEntryType.Status);
                        }
                        else
                        {
                            // Find a corresponding xml object
                            // field with Name in the name attribute is cI.Name
                            // <fld name='Wave Name' type='String'>burp2.pk</fld>
                            XElement cXel = null;
                            try
                            {
                                cXel = (from m in AllObjects
                                        from x in m.Elements()
                                        where x.Attributes().Any(h => h.Name == "name" && h.Value == "Body")
                                        from y in m.Elements()
                                        where y.Value == rCI.Name
                                        select m).LastOrDefault();
                            }
                            catch (Exception ex)
                            {
                                me.Msg = string.Format("{0} component EXCEPTION thrown ({1}).", rCI.Name, ex.Message);
                                me.Number = objNumber + 1;
                                Log.AddEntry(me, LogBookEntryType.Error);
                            }
                            if (cXel == null)
                            {
                                me.Msg = string.Format("{0} component is not found.", rCI.Name);
                                me.Number = objNumber + 1;
                                Log.AddEntry(me, LogBookEntryType.Error);
                                continue;
                            }
                            // Fill all the field for rCI from cXel

                            /////////////////////////////////
                            // CREATE NEW ENTITY FOR COMPONENT

                            switch (rCI.Type)
                            {
                                case ComponentType.Setup:
                                    break;
                                case ComponentType.PulseProgram:
                                    rCEntity = new Pulseprogram();
                                    rCEntity.Content = (from x in cXel.Elements()
                                                        where x.Attribute("name") != null && x.Attribute("name").Value == "Body"
                                                        select x.Value).Single();
                                    rCEntity.Script = "# Not applicable.";
                                    goto default;
                                case ComponentType.WaveShape:
                                    rCEntity = new Wave();
                                    rCEntity.Content = (from x in cXel.Elements()
                                                        where x.Attribute("name") != null && x.Attribute("name").Value == "Body"
                                                        select x.Value).Single();
                                    rCEntity.Script = string.Format("# Power level factor{1}ExpScope.AddParameter(\"components.{0}.factor\", 1.0){1}", rCI.Identifier, Environment.NewLine);
                                    goto default;
                                case ComponentType.CPD:
                                    rCEntity = new Cpd();
                                    rCEntity.Content = (from x in cXel.Elements()
                                                        where x.Attribute("name") != null && x.Attribute("name").Value == "Body"
                                                        select x.Value).Single();
                                    rCEntity.Script = "# Not applicable.";
                                    goto default;
                                case ComponentType.Script:
                                    rCEntity = new Script();
                                    ((Script)rCEntity).Type = "Not Available";
                                    ((Script)rCEntity).IsIncludable = false;
                                    rCEntity.Script = (from x in cXel.Elements()
                                                       where x.Attribute("name") != null && x.Attribute("name").Value == "Body"
                                                       select x.Value).Single();
                                    rCEntity.Content = string.Empty;
                                    goto default;
                                default:
                                    rCEntity.Content = RemoveHeadSpacesInAllLines(rCEntity.Content);

                                    rCEntity.Id = Guid.NewGuid();
                                    rCEntity.Datetime = DateTime.Now;
                                    rCEntity.IsPrivate = false;
                                    rCEntity.IsDraft = false;
                                    rCEntity.VersionInfo = "Imported from CARA database";
                                    rCEntity.FirstVersion = rCEntity;
                                    rCEntity.Template = rCEntity;

                                    rCEntity.Name = rCI.Name;
                                    string info = (from field in cXel.Elements()
                                                   where field.Attribute("name") != null && field.Attribute("name").Value == "Memo"
                                                   select field.Value).SingleOrDefault();
                                    rCEntity.Info = info == null ? "Not available" : info;
                                    
                                    break;
                            }
                            uniqueComponents.Add(rCEntity.Name, rCEntity);

                            componentsCounter++;
                            me.Number = objNumber + 1;
                            me.Msg = string.Format("{0} component is NEW.", rCI.Name);
                            Log.AddEntry(me, LogBookEntryType.Status);

                            inmr.AddToBaseObjects(rCEntity);

                            NumberedAuthor na = new NumberedAuthor();
                            na.Index = 0;
                            na.Id = Guid.NewGuid();
                            na.NMRObject = rCEntity;
                            inmr.AddToNumberedAuthors(na);
                            admin.Authorships.Add(na);
                        }

                        NamedComponent nc = new NamedComponent();
                        nc.Component = rCEntity;
                        nc.Id = Guid.NewGuid();
                        nc.Identifier = rCI.Identifier;
                        inmr.AddToNamedComponents(nc);

                        currentSetup.NamedComponents.Add(nc);
                        /////////////////////////////////
                    }
                    Log.DefaultLevel--;

                    // Admin is author
                    NumberedAuthor a = new NumberedAuthor();
                    a.Index = 0;
                    a.Id = Guid.NewGuid();
                    a.NMRObject = currentSetup;

                    inmr.AddToNumberedAuthors(a);

                    // Saving to database
                    admin.Authorships.Add(a);
                    inmr.SaveChanges();

                    WikiPagesManager.CreateWikiPage(ComponentType.Setup, currentSetup.Id, currentSetup.Name);
                }
                foreach (Component c in uniqueComponents.Values)
                {
                    WikiPagesManager.CreateWikiPage(c.GetComponentType(), c.Id, c.Name);
                }
            }
        }

        #region PRIVATE MEMBERS

        /// <summary>
        /// The function parses lua code and converts it to Python
        /// It also extracts related components
        /// 
        /// </summary>
        /// <param name="luaSetupContent"></param>
        /// <param name="relatedComponents"></param>
        /// <returns>Python code + relatedcomponent list (components are unique in there)</returns>
        private string ConvertFromLuaToPython(string luaSetupContent, out List<ComponentItem> relatedComponents, int objNumber)
        {
            LogMessage me = new LogMessage();

            relatedComponents = new List<ComponentItem>();

            Dictionary<string, ComponentItem> relComps = new Dictionary<string, ComponentItem>();

            #region Get The Content inside '{}'
            int beg = luaSetupContent.IndexOf('{');
            int end = luaSetupContent.IndexOf('}');
            string luaInside = luaSetupContent.Substring(beg + 1, end - beg - 1);
            string luaOutside = "#" + luaSetupContent.Substring(end + 1).Replace("\n", "\n#");

            #endregion

            #region Split content into the lines
            char[] delimiters = new char[] { '\r', '\n' };
            string[] luaLines = luaInside.Split(delimiters);
            #endregion

            StringBuilder pythonMaster = new StringBuilder();
            pythonMaster.AppendLine(
@"import math

# Function to calculate the power of a shaped pulse.
def CalcPower(cPulse90, refShapeConst, calib90pulse, calibPower):
    reference_rect_pulse90 = 10.
    reference_rect_power_rf=50000.
    calibration_constant_db = 0.
    out_put = 120
    out_put = 20.*math.log10(refShapeConst*cPulse90/(calib90pulse))+calibPower
    return out_put");


            StringBuilder pythonCode = new StringBuilder();

            #region Process Lines separately
            this.Log.DefaultLevel++;
            foreach (string luaLine in luaLines)
            {
                pythonCode = new StringBuilder();
                string comment = string.Empty;
                string luaCode = luaLine;
                // Getting Comment
                if (luaLine.Contains("--"))
                {
                    int commentPosition = luaLine.IndexOf("--"); // Everything from this position is a comment
                    comment = "#  " + luaLine.Substring(commentPosition + 2);

                    luaCode = luaLine.Remove(commentPosition);
                }

                // Remove terminal ',' and spaces from the code line if present
                luaCode = luaCode.Trim(' ', ',');

                // We assume that only assignmnets are made in lua code
                string leftSide = null, rightSide = null;
                if (luaCode.Contains('='))
                {
                    int equalPosition = luaCode.IndexOf('=');
                    leftSide = luaCode.Substring(0, equalPosition).Trim();
                    rightSide = luaCode.Substring(equalPosition + 1).Trim();

                    leftSide = ProcessLeftSide(leftSide);
                    rightSide = ProcessRightSide(rightSide, objNumber);

                    Match fileCommand = Regex.Match(leftSide, "(pulprog|cpdprg|spnam|script)");
                    if (fileCommand.Success)
                    {
                        ComponentItem oppa = new ComponentItem();
                        oppa.Id = Guid.NewGuid();
                        oppa.Name = rightSide.Trim('"');
                        oppa.Identifier = rightSide.Trim('"').Replace('.', '_');
                        switch (fileCommand.Value)
                        {
                            case "pulprog":
                                oppa.Type = ComponentType.PulseProgram;
                                break;
                            case "cpdprg":
                                oppa.Type = ComponentType.CPD;
                                break;
                            case "spnam":
                                oppa.Type = ComponentType.WaveShape;
                                break;
                            case "script":
                                oppa.Type = ComponentType.Script;
                                break;
                            default:
                                break;
                        }

                        AddComponentToCollection(oppa, relComps);
                        //relatedComponents.Add(oppa);
                        pythonCode.AppendFormat("ExpScope.BrukerFileCommand(\"{0}\", \"components.{1}\")", leftSide, oppa.Identifier);
                    }
                    else
                    {
                        pythonCode.AppendFormat("ExpScope.BrukerCommand(\"{0}\", {1})", leftSide, rightSide);
                    }
                }
                else if (!string.IsNullOrEmpty(luaCode))
                {
                    pythonCode.AppendFormat("# Unparsed lua code: {0}.", luaCode);

                    me.Msg = string.Format("Unparsed lua code: {0}.", luaCode);
                    me.Number = objNumber;
                    this.Log.AddEntry(me, LogBookEntryType.Error);
                }
                else
                {
                    pythonCode.Append(string.Empty);
                }

                if (!string.IsNullOrEmpty(pythonCode.ToString()))
                {
                    pythonMaster.AppendFormat("{0} {1}{2}", pythonCode, comment, Environment.NewLine);
                }
                else
                {
                    pythonMaster.AppendFormat("{0}{1}", comment, Environment.NewLine);
                }
                continue;
            #endregion
            }
            this.Log.DefaultLevel--;
            // Prepare to return
            relatedComponents = relComps.Values.ToList<ComponentItem>();
            return pythonMaster.ToString();
        }

        private string ProcessLeftSide(string lS)
        {
            // Open up '[""]'
            return lS.Replace("[\"", string.Empty).Replace("\"]", string.Empty);
        }

        private string ProcessRightSide(string rS, int objNumber)
        {
            double num;
            if (double.TryParse(rS, out num)) return rS;

            // CHeck for '"' on the sides
            // Means its a file or xcpr
            if (rS[0] == '"' && rS.Last() == '"') return rS;

            //// Check for Function Call
            //Match parameters = Regex.Match(rS, @"calc_power_set\((.+),(.+),(.+),(.+)\)");
            //if (parameters.Success)
            //{
            //    if (parameters.Groups.Count != 5)
            //    {
            //        me.Number = objNumber+1;
            //        me.Msg = string.Format("Partial calc_power_set match in line '{0}'.", rS);
            //        this.Log.AddEntry(me, LogBookEntryType.Error);
            //        return rS;
            //    }
            //    string result = string.Format("CalcPower(ExpScope.GetValue(\"{0}\"), {1}, ExpScope.GetValue(\"{2}\"), ExpScope.GetValue(\"{3}\"))", VariableLookup(parameters.Groups[1].Value, objNumber), parameters.Groups[2].Value, VariableLookup(parameters.Groups[3].Value, objNumber), VariableLookup(parameters.Groups[4].Value, objNumber));
            //    return result;
            //}

            // Check for ex[pression
            Match parameters = Regex.Match(rS, @"((?:[a-zA-Z][\w\[\]]+)(?:\.[a-zA-Z][\w\[\]]*)+)");
            while (parameters.Success)
            {
                foreach (Capture xh in parameters.Groups[0].Captures)
                {
                    string comm = string.Format("ExpScope.GetValue(\"{0}\")", VariableLookup(xh.Value, objNumber));
                    rS = rS.Replace(xh.Value, comm);
                }
                parameters = parameters.NextMatch();
            }
            rS = rS.Replace("calc_power_set", "CalcPower");

            return rS;
            // return VariableLookup(rS, objNumber);
        }

        private string VariableLookup(string luaVar, int objNumber)
        {
            string pythonVar = luaVar;
            bool test = VarTransforms.ContainsKey("spectrometer[now].B_field");
            bool test1 = VarTransforms.ContainsKey(luaVar);
            if (VarTransforms.ContainsKey(luaVar)) return VarTransforms[luaVar];

            LogMessage me = new LogMessage()
            {
                Msg = string.Format("{0} lua variable is not found in the transformation list.", luaVar),
                Number = objNumber + 1
            };
            Log.AddEntry(me, LogBookEntryType.Error);
            return pythonVar;
        }

        private void AddComponentToCollection(ComponentItem cI, Dictionary<string, ComponentItem> collection)
        {
            if (collection.ContainsKey(cI.Name)) return;
            collection.Add(cI.Name, cI);
        }

        private string RemoveHeadSpacesInAllLines(string text)
        {
            List<string> lines = text.Split('\n').ToList<string>();
            if (string.IsNullOrEmpty(lines[0]))
            {
                lines.RemoveAt(0);
            }
            for(int i =0; i< lines.Count; i++)
            {
                lines[i] = lines[i].Trim();
            }
            return string.Join(Environment.NewLine, lines.ToArray());
        }
        #endregion
    }
}
